/**
 * Copyright (c) <2010>, <SUNNY GUPTA>
 * All rights reserved.

 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the <ORGANIZATION> nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <SUNNY GUPTA> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.gwtp.investmentadvisor.server.core.datastore;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.jdo.annotations.NotPersistent;
import javax.persistence.Embedded;
import javax.persistence.EntityExistsException;
import javax.persistence.Id;
import javax.persistence.Transient;

import com.google.appengine.api.datastore.EntityNotFoundException;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.Query;
import com.googlecode.objectify.helper.DAOBase;
import com.gwtp.investmentadvisor.server.authentication.entities.SessionIDToken;
import com.gwtp.investmentadvisor.server.authentication.entities.UserAccount;
import com.gwtp.investmentadvisor.server.core.exception.NotSupportedException;
import com.gwtp.investmentadvisor.server.entities.finance.FinancialMarket;
import com.gwtp.investmentadvisor.server.entities.finance.FinancialSecurity;
import com.gwtp.investmentadvisor.server.entities.finance.FinancialSecurityQuote;
import com.gwtp.investmentadvisor.server.entities.general.Country;

/**
 * <p>Wrapper class over {@link com.googlecode.objectify.ObjectifyFactory}.</p>
 * <p>It's OK to create multiple instances of this class, since the class itself is very light, and all such
 * instances will use a singleton of ObjectifyFactory and hence the overall scheme will remain slim</p>
 * <p>You may want to extend this generic DAO into domain specific DAOs to provide better helper functions</p>
 * @author David Chandler
 * @author Sunny Gupta
 *
 * @param <T> Entity class for which Data Access Object is required
 */
public class ObjectifyGenericDAO<T> extends DAOBase {

  static final int BAD_MODIFIERS = Modifier.FINAL | Modifier.STATIC | Modifier.TRANSIENT;

  static {
    // Register all your entity classes here
    ObjectifyService.register(UserAccount.class);
    ObjectifyService.register(SessionIDToken.class);
    ObjectifyService.register(Country.class);
    ObjectifyService.register(FinancialSecurity.class);
    ObjectifyService.register(FinancialSecurityQuote.class);
    ObjectifyService.register(FinancialMarket.class);
  }

  protected Class<T> clazz;
  
  @SuppressWarnings("unchecked")
  public ObjectifyGenericDAO() {
    // Init superclass without Txn
    super();
    this.clazz = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    // Check if annotated with NotPersisted and throw error
    if (this.clazz.isAnnotationPresent(NotPersistent.class)) {
      throw new NotSupportedException("This Entity is marked as Not Persisted, " +
          "and hence cannot create a DAO");
    }
  }
  
  public Key<T> put(T entity) {
    return ofy().put(entity);
  }

  public Map<Key<T>, T> putAll(Iterable<T> entities) {
    return ofy().put(entities);
  }

  public void delete(T entity) {
    ofy().delete(entity);
  }

  public void deleteKey(Key<T> entityKey) {
    ofy().delete(entityKey);
  }

  public void deleteAll(Iterable<T> entities) {
    ofy().delete(entities);
  }

  public void deleteKeys(Iterable<Key<T>> keys) {
    ofy().delete(keys);
  }

  public T get(Long id) throws EntityNotFoundException {
    return ofy().get(this.clazz, id);
  }
  
  public T get(long id) throws EntityNotFoundException {
    return ofy().get(this.clazz, id);
  }
  
  public T get(String id) throws EntityNotFoundException {
    return ofy().get(this.clazz, id);
  }
  
  public T get(Key<T> key) throws EntityNotFoundException {
    return ofy().get(key);
  }

  /**
   * Convenience method to get all objects matching a single property.
   *
   * @param propName
   * @param propValue
   * @return T matching Object
   */
  public T getByProperty(String propName, Object propValue) {
    Query<T> q = ofy().query(clazz);
    q.filter(propName, propValue);
    return q.get();
  }

  public List<T> listByProperty(String propName, Object propValue) {
    Query<T> q = ofy().query(clazz);
    q.filter(propName, propValue);
    // q.fetch not really needed
    return asList(q);
  }

  public List<Key<T>> listKeysByProperty(String propName, Object propValue) {
    Query<T> q = ofy().query(clazz);
    q.filter(propName, propValue);
    return asKeyList(q.fetchKeys());
  }

  public T getByExample(T exampleObj) {
    Query<T> queryByExample = buildQueryByExample(exampleObj);
    // q.fetch not really needed
    Iterable<T> iterableResults = queryByExample;
    Iterator<T> i = iterableResults.iterator();
    T obj = i.next();
    if (i.hasNext()) {
      throw new RuntimeException("Too many results");
    }
    return obj;
  }

  public List<T> listByExample(T exampleObj) {
    Query<T> queryByExample = buildQueryByExample(exampleObj);
    // q.fetch not really needed
    return asList(queryByExample);
  }

  /**
   * <p>Looks up for any existing obj with the ID, and adds if not present.</p>
   * <p>All actions take place within a transaction and hence it guarantees that no duplicates are creates
   *  nor is an existing entity with the same ID overwritten</p>
   * @param obj
   */
  public void addNew(T obj) {    
    // First obtain ID
    Object id = null;
    // flag to check for duplicate ID field
    boolean idFound = false;
    for (Field field : clazz.getDeclaredFields()) {
      if (field.isAnnotationPresent(Id.class)) {
        // Check for duplicate ID fields
        if (idFound) {
          throw new IllegalStateException("Multiple @Id fields found in " +
              "the class hierarchy of " + this.clazz.getName());
        }
        idFound = true;
        
        field.setAccessible(true);

        // Try to obtain ID content
        try {
          id = field.get(obj);
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
      }
    }
    
    // Check if id found
    if (id != null) {
      // Obtain key
      Key<T> key = null;
      // This covers both long & Long
      if (id instanceof Long) {
        key = new Key<T>(clazz, ((Long) id).longValue());
      }
      
      if (id instanceof String) {
        key = new Key<T>(clazz, (String) id);
      }
      
      // Perform transaction
      Objectify ofy = fact().beginTransaction();
      
      // First check for existing entity
      if (ofy.find(key) != null) {
        // Exists
        throw new EntityExistsException();
      } else {
        // Not yet exists, put and commit
        ofy.put(obj);
        
        try {
          ofy.getTxn().commit();
        
          // Commit succeeded
        } finally {
          if (ofy.getTxn().isActive()) {
            ofy.getTxn().rollback();
            // Failed, Exists
            throw new EntityExistsException();
          }
        }
      }
    } else {
      throw new IllegalStateException("No @Id field found in " +
          "the class hierarchy of " + this.clazz.getName());
    }
  }
  
  private List<T> asList(Iterable<T> iterable) {
    ArrayList<T> list = new ArrayList<T>();
    for (T t : iterable) {
      list.add(t);
    }
    return list;
  }

  private List<Key<T>> asKeyList(Iterable<Key<T>> iterableKeys) {
    ArrayList<Key<T>> keys = new ArrayList<Key<T>>();
    for (Key<T> key : iterableKeys) {
      keys.add(key);
    }
    return keys;
  }

  private Query<T> buildQueryByExample(T exampleObj) {
    Query<T> q = ofy().query(clazz);

    // Add all non-null properties to query filter
    for (Field field : clazz.getDeclaredFields()) {
      // Ignore transient, embedded, array, and collection properties
      if (field.isAnnotationPresent(Transient.class)
        || (field.isAnnotationPresent(Embedded.class))
        || (field.getType().isArray())
        || (Collection.class.isAssignableFrom(field.getType()))
        || ((field.getModifiers() & BAD_MODIFIERS) != 0)) {
        continue;
      }

      field.setAccessible(true);

      Object value;
      try {
        value = field.get(exampleObj);
      } catch (IllegalArgumentException e) {
        throw new RuntimeException(e);
      } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
      }
      
      if (value != null) {
        q.filter(field.getName(), value);
      }
    }

    return q;
  }
}
